#
# GameBackend class - encapsulates the uplink to the FGS backend.
#

from twisted.python import log
from twisted.web.xmlrpc import Proxy
from twisted.internet import reactor
from twisted.internet.defer import Deferred, succeed, fail
from time import time

from fgs.proxy import error as fgs_error
from fgs.proxy.backend.iface import IGameBackend

class BackendXmlRpc(IGameBackend):
    def __init__(self, config):
        import logging

        # Check configuration
        config.check_fields(config.check_failure_exception,
                            ('backend:xmlrpc', 'url'),
                            ('backend:xmlrpc', 'key'),
                            ('backend:xmlrpc', 'password'))

        # Load configuration
        self._backend_uri = config.get('backend:xmlrpc', 'url')
        self._backend_key = config.get('backend:xmlrpc', 'key')
        self._backend_passwd = config.get('backend:xmlrpc', 'password')

        # Initialize the proxy, and the variables used by the backend
        # connector.
        self._backend = Proxy(self._backend_uri)
        self._sid = None
        self._last_heartbeat = time()
        self._request_queue = []
        self._server_map = {} # Maps gameserver objects to server UUIDs
        self._match_map = {} # Maps match objects to gameserver objects
        self._player_map = {} # Maps player objects to their auth credentials
        self._match_player_map = {} # Maps match objects to dicts
                                    # mapping players to match-unique
                                    # numeric indices

        # Initialize log
        self._log = logging.getLogger('backend.xmlrpc')

    def _send_failure(self, e):
        self._log.error("Complete and utter failure: %s", e.value)
        self._log.debug("Traceback of failure: %s", e.getTraceback())
        reactor.stop()

    def _send_request(self, method, *args):
        """Queue the given request for sending to the backend.  Return
        a Deferred returning the call's result, or FgsError if the
        call failed."""

        d = Deferred()
        self._request_queue.append((d, method, args))
        if len(self._request_queue) == 1:
            self._process_requests()

        return d

    def _process_requests(self):
        def process_queue(res):
            self._request_queue.pop(0)
            if len(self._request_queue) > 0:
                self._process_requests()
            return res
        def success(res):
            return res
        def error(e):
            from twisted.python.failure import Failure
            self._log.error("RPC request failed: %s", e.value)
            raise fgs_error.FgsProxyError(e.value)

        self._log.debug("%d requests in XMLRPC request queue",
                        len(self._request_queue))

        req = self._request_queue[0]
        res = self._backend.callRemote(req[1], *req[2])
        res.addBoth(process_queue)
        res.addCallbacks(success, error)
        res.chainDeferred(req[0])

    def _do_heartbeat(self):
        """This function implements a task that recurs every 30
        seconds.  It has to provide the backend with a heartbeat
        signal, to indicate that the proxy is still there.  This can
        mean either actually registering with the proxy, pinging it,
        or trying to reestablish contact after a heartbeat failure."""

        def schedule_heartbeat(_):
            """Closing callback and errback: schedule the next beat."""
            self._log.debug("Next heartbeat in 30 seconds...")
            self._heartbeat_task = reactor.callLater(30, self._do_heartbeat)

        def test_timeout(_):
            """Beat failure errback.  Determine if we've been failing
            for too long, and if so, mark the connection offline."""
            self._log.debug("Testing if timeout reached...")
            if self._sid and (time() - self._last_heartbeat > 120):
                self._log.warning("Backend heartbeat timed out, switching proxy to autonomous mode")
                self._sid = None

        def record_heartbeat(_):
            """Beat success callback.  Record the time of latest
            successful beat, for future reference in the timout
            algorithm."""
            self._last_heartbeat = time()

        #
        # Two different behaviours. Either we're not connected...
        #
        if not self._sid:
            def success(sid):
                self._log.info("Registration successful")
                self._sid = sid
            def error(e):
                self._log.error("Registration to backend failed")
                self._log.info("Registration error: %s", e.value)
                raise e

            self._log.info("Trying to register with the backend...")
            res = self._backend.callRemote('fgsd.register', self._backend_key,
                                           self._backend_passwd, int(time()))
            res.addCallbacks(success, error)

        #
        # Or we're already linked, we just need to ping...
        #
        else:
            def success(_):
                self._log.info("Ping successful")
            def error(e):
                self._log.error("Backend ping failed")
                self._log.info("Ping error: %s", e.value)
                raise e

            self._log.info("Trying to ping backend...")
            res = self._backend.callRemote('fgsd.ping', self._sid, int(time()))
            res.addCallbacks(success, error)

        # ... And for both modes: do some bookeeping, and reschedule a
        # new beat.
        res.addCallbacks(record_heartbeat, test_timeout)
        res.addBoth(schedule_heartbeat)


    def register(self):
        # Registration actually jumpstarts a frozen heart, or kicks a
        # running heart to get going faster.
        if hasattr(self, '_heartbeat_task'):
            self._heartbeat_task.cancel()
        self._do_heartbeat()


    def unregister(self):
        def success(res):
            self._log.info("Unregistration successful")
            return True
        def error(e):
            self._log.error("Unregistration failed")
            self._log.info("Unregistration error: %s", e.value)
            return False

        # Cancel the heartbeat loop
        if hasattr(self, '_heartbeat_task'):
            self._heartbeat_task.cancel()

        # If we were registered at the time of death, unregister.
        if self._sid != None:
            self._log.debug("Unregistering from backend")
            res = self._backend.callRemote('fgsd.unregister', self._sid)
            res.addCallbacks(success, error)
            return res
        else:
            return succeed(0)

    def server_register(self, game_server):
        # If we're not connected, fail.
        if not self._sid:
            return fail(fgs_error.FgsProxyBackendUnavailableError())

        def success(sid):
            self._log.debug("Registration successful, SID = %s", sid)
            self._server_map[game_server] = sid
            return game_server

        d = self._send_request('fgsd.server.register', self._sid,
                               game_server.game_name, game_server.version,
                               game_server.port)
        d.addCallback(success)

        return d

    def server_unregister(self, game_server):
        def success(_):
            self._log.debug("Server unregistration successful.")
        def error(e):
            self._log.debug("Server unregistration failed: %s", e.value)

        gameserver_sid = self._server_map[game_server]
        del self._server_map[game_server]

        d = self._send_request('fgsd.server.unregister', self._sid,
                               gameserver_sid)
        d.addCallbacks(success, error)

    def player_update(self, player, update_data):
        def success(_):
            self._log.debug("Player update successful")
        def error(e):
            self._log.debug("Player update error: ", e.value)

        # If the user isn't authenticated, he doesn't get an update.
        if player not in self._player_map.keys():
            return

        self._log.debug("Committing player update to server...")

        # Translate the unified representation into BackendCoinCoin
        # representation.
        player_updates = dict([(k, player[k]) for k in update_data])

        # Retrieve the user credentials from our map
        creds = self._player_map[player]

        from time import time
        d = self._send_request('fgsd.user.update', self._sid,
                               creds[0], creds[1], creds[2],
                               player_updates, int(time()))
        d.addCallbacks(success, error)

    def server_new_match(self, game_server, match):
        # Add the important match metadata to the match dict and
        # commit it. That will trigger an update_match call that will
        # do the actual calling.
        match["GameID"] = game_server.game_name
        match["GameSID"] = self._server_map[game_server]
        match["GameVersion"] = game_server.version
        match['GameStatus'] = "Running"
        self._match_map[match] = game_server
        self._match_player_map[match] = {'_max': 0}
        self._log.debug("Setting up basic match data")
        self._log.debug("Basic match data: %s" % match.__repr__())
        match.commit()

    def match_add_player(self, match, player):
        player_match_info = match.players[player.uuid]
        if player_match_info not in self._match_player_map[match].keys():
            new_id = self._match_player_map[match]['_max'] + 1
            self._match_player_map[match][player_match_info] = new_id
            self._match_player_map[match]['_max'] = new_id
            self._log.debug("Added player %s to match %s",
                            player.uuid, match.uuid)

    def match_remove_player(self, match, player):
        player_match_info = match.players[player.uuid]
        if player_match_info in self._match_player_map[match].keys():
            self._log.debug("Removing player from match")
            # XXX: Should we do more/less here?  Set the player's
            # match status to disconnected, commit the match, and
            # remove the player's tracking info from the backend.
            player_match_info["PlayerStatus"] = "Disconnected"
            match.commit()
            del self._match_player_map[match][player_match_info]

    def match_update(self, match, match_updates, player_updates):
        def success(_):
            self._log.debug("Match update successful")
        def error(e):
            self._log.debug("Match update error: %s", e.value)

        self._log.debug("Committing match update to server...")

        gameserver_sid = self._server_map[self._match_map[match]]

        # Translate the unified representation into BackendCoinCoin
        # representation.
        self._log.debug("Match updates, unified: %s", match_updates)
        self._log.debug("Player updates, unified: %s", player_updates)

        match_updates = dict([(x,match[x]) for x in match_updates])
        self._log.debug("Match/Player map: %s",
                        self._match_player_map[match])
        player_updates = dict([(str(self._match_player_map[match][k]),
                                dict([(x,k[x]) for x in v]))
                               for k,v in player_updates.items()])

        self._log.debug("Match updates, CoinCoin: %s", match_updates)
        self._log.debug("Player updates, CoinCoin: %s", player_updates)

        d = self._send_request('fgsd.server.update', self._sid,
                               gameserver_sid, match.uuid,
                               match_updates, player_updates,
                               int(time()))
        d.addCallbacks(success, error)

    def match_end(self, game_server, match):
        def success(_):
            self._log.debug("Match lock successful")
        def error(e):
            self._log.error("Match lock error: %s", e.value)

        self._log.info("Locking match %s" % match.uuid)
        match.commit()
        gameserver_sid = self._server_map[game_server]
        d = self._send_request("fgsd.server.lock", self._sid,
                               gameserver_sid, match.uuid)
        d.addCallbacks(success, error)

    def user_verify(self, login, token, nonce):
        def success(_):
            self._log.debug("Player %s authentication successful", login)
            return True
        def error(e):
            self._log.debug("Player %s authentication failed", login)
            self._log.debug("Error was: %s", e.value)
            return False

        d = self._send_request('fgsd.user.verify', self._sid, login,
                               token, nonce)
        d.addCallbacks(success, error)

        return d

    def user_join(self, game_server, player, login, token, nonce):
        self._player_map[player] = (login, token, nonce)
        self._log.info("Should notify backend of authenticated player join here, but it doesn't care yet")

    def user_leave(self, game_server, player):
        # If the player isn't authenticated, nothing to do
        if player in self._player_map.keys():
            del self._player_map[player]
            self._log.info("Should notify backend of authenticated player leave here, but it doesn't care yet")

backend_class = BackendXmlRpc
